/*!	\file myfs_dir.c
 *
 *	в данном файле хранятся функции работы с директориями
 *
 */

#include <linux/fs.h>
#include <linux/buffer_head.h>
#include <linux/pagemap.h>
#include <linux/swap.h>

#include "myfs_inode.h"

/*!
	функция проверяет, разрешен ли данный режим доступа для файла, связанного с данным и-узлом
*/
int myfs_permission(struct inode *inode, int mask)
{
	return generic_permission(inode, mask, NULL);
}

/*!
	функция очищает память, выделенную под страницу
*/	
static inline void myfs_put_page(struct page *page)
{
	kunmap(page);
	page_cache_release(page);
}

/*!
	возвращает количество страниц, занимаемых данными и-узла
*/
static inline int myfs_inode_pages(struct inode *inode)
{
	return (inode->i_size + PAGE_CACHE_SIZE - 1) / PAGE_CACHE_SIZE;
}

/*!
	возвращает страницу, прочитанную в кэше страниц.
	в случае ошибки выделяет память под новую страницу и возвращает ее
*/
static struct page * myfs_get_page(struct inode *dir, int n)
{
	struct address_space *as = dir->i_mapping;
	struct page * page = read_mapping_page(as, n, NULL);

	if (!IS_ERR(page))
	{
		kmap(page);
	}
	return page;
}

/*!
	функция возвращает смещение последнего значимого байта в странице
*/
static unsigned myfs_last_byte_for_page(struct inode *inode, int pagenum)
{
	unsigned last_byte = inode->i_size;
	last_byte -= pagenum << PAGE_CACHE_SHIFT;
	return (last_byte > PAGE_CACHE_SIZE ? PAGE_CACHE_SIZE : last_byte);
}

/*!
	функция сравнивает две строки. 
	возвращает 1 если они идентичны, иначе 0
*/
static inline int myfs_match(int len, const char *name , struct myfs_dir_entry *de)
{
	if (len != de->name_len)
		return 0;
	if (!de->inode)
		return 0;
	return !memcmp(name, de->name, len);
}

/*!
	функция устанавливает тип для элемента директории
*/
void myfs_set_inode_type(struct myfs_dir_entry *dentry, struct inode *inode)
{
	dentry->file_type = 0;
	if(S_ISDIR(inode->i_mode))
	{
		dentry->file_type = S_IFDIR;
	}
	else 
		if(S_ISREG(inode->i_mode))
		{
			dentry->file_type = S_IFREG;
		}
	return;
}

static int myfs_commit_chunk(struct page *page, loff_t pos, unsigned len)
{
	struct address_space *mapping = page->mapping;
	struct inode *dir = mapping->host;
	int err = 0;

	block_write_end(NULL, mapping, pos, len, len, page, NULL);

	if(pos+len > dir->i_size)
	{
		i_size_write(dir, pos+len);
		mark_inode_dirty(dir);
	}

	err = write_one_page(page,1);
	if (!err)
		err = myfs_write_inode(dir,1);

	return err;
}

/*!
	функция создания пустой директории
*/
int myfs_make_empty(struct inode *inode, struct inode *dir)
{
	struct myfs_dir_entry *de;
	__u32 reclen_1;
	__u32 reclen_2;

	struct page *page = grab_cache_page(inode->i_mapping, 0);
	struct address_space *mapping = page->mapping;

	char *kaddr;
	int err;

	reclen_1 = calc_reclen_from_len(1);
	reclen_2 = MYFS_DFLT_BLOCKSIZE - calc_reclen_from_len(1);

	if (!page)
		return -ENOMEM;

	err = __myfs_write_begin(NULL, mapping, 0, reclen_1 + reclen_2, 0, &page, NULL);

	if (err)
	{
		unlock_page(page);
		goto fail;
	}

	kaddr = kmap_atomic(page, KM_USER0);
	memset(kaddr, 0, PAGE_CACHE_SIZE);

	de = (struct myfs_dir_entry *)kaddr;

	de->inode = inode->i_ino;
	strncpy(de->name, ".", 1);
	de->file_type = S_IFDIR;
	de->rec_len = reclen_1;
	de->name_len = 1;
	de = (void*)((char*)de + reclen_1);

	de->inode = dir->i_ino;
	strncpy(de->name, "..", 2);
	de->file_type = S_IFDIR;
	de->rec_len = reclen_2;
	de->name_len = 2;

	kunmap_atomic(kaddr, KM_USER0);

	err = myfs_commit_chunk(page, 0, reclen_1 + reclen_2);
fail:
	page_cache_release(page);
	return err;
}

/*!
	функция проверяет директорию на наличие элементов в ней.
	возвращает 1 если директория пуста, в противном случае возвращает 0
*/
int myfs_empty_dir(struct inode * inode)
{
	struct page *page = NULL;
	unsigned long i, npages = myfs_inode_pages(inode);
	char *name;
	__u32 inumber;

	for (i = 0; i < npages; i++)
	{
		char *kaddr, *limit;
		struct myfs_dir_entry *de;

		page = myfs_get_page(inode, i);
		if (IS_ERR(page))
			continue;

		kaddr = (char *)page_address(page);
		limit = kaddr + myfs_last_byte_for_page(inode, i) - calc_reclen_from_len(1);
		
		for (de = (struct myfs_dir_entry *)kaddr; (char *)de <= limit; de = (struct myfs_dir_entry *)((char *)de + de->rec_len))
		{
			//myfs_dir_entry *d_entry = (struct myfs_dir_entry *)de;
			name = de->name;
			inumber = de->inode;

			if (inumber != 0)
			{
				if (name[0] != '.')
					goto not_empty;
					
				if (!name[1]) 
				{
					if (inumber != inode->i_ino)
						goto not_empty;
				} 
				else 
					if (name[1] != '.')
						goto not_empty;
					else 
						if (name[2])
							goto not_empty;
			}
		}
		myfs_put_page(page);
	}
	
	return 1;

not_empty:
	myfs_put_page(page);
	
	return 0;
}

/*!
	функция добавления нового элемента в директорию
*/
int myfs_add_link(struct dentry *dentry, struct inode *inode)
{
	struct inode *dir = dentry->d_parent->d_inode;
	struct page *page = NULL;
	unsigned long npages = myfs_inode_pages(dir);
	loff_t pos;
	struct myfs_dir_entry *de;
	__u32 reclen = calc_reclen_from_len(dentry->d_name.len);
	char *kaddr;
	int i, err=0;
	int namelen = dentry->d_name.len;
	int rec_len, name_len;
	const char *name = dentry->d_name.name;

	BUG_ON(!(npages <= 1 || npages <= (dir->i_sb->s_blocksize / PAGE_CACHE_SIZE)));

	for (i = 0; i <= npages; i++)
	{
		char *dir_end;
		page = myfs_get_page(dir, i);

		if (IS_ERR(page))
			goto out;

		lock_page(page);
		kaddr = (char*)page_address(page);
		dir_end = kaddr + myfs_last_byte_for_page(dir, i);
		de = (struct myfs_dir_entry *)kaddr;

		kaddr += PAGE_CACHE_SIZE - reclen;
		while ((char * )de <= kaddr)
		{
			if ((char *)de == dir_end)
			{
				name_len = 0;
				rec_len = dir->i_sb->s_blocksize;
				de->inode = 0;
				de->rec_len = rec_len;
				goto gotit;
			}
			BUG_ON(de->rec_len == 0);
			printk(KERN_ALERT "myfs: got \"%s\" : %u reclen = %d.\n",de->name, de->inode, de->rec_len);
			err = -EEXIST;
			if (myfs_match(namelen, name, de))
				goto page_unlock;

			rec_len = de->rec_len;
			name_len = calc_reclen_from_len(de->name_len);

			if(!de->inode && rec_len >= reclen)
			{
				goto gotit;
			}

			if(rec_len >= name_len + reclen)
			{
				goto gotit;
			}	
			de = (struct myfs_dir_entry *)((char *)de+rec_len);
		}
		unlock_page(page);
		myfs_put_page(page);
	}
	BUG();
	return -EINVAL;
gotit:
	pos = (page->index >> PAGE_CACHE_SHIFT) + (char *)de - (char *)page_address(page);
	err = __myfs_write_begin(NULL, page->mapping, pos, rec_len, 0, &page, NULL);
	if (err)
		goto page_unlock;
	if (de->inode)
	{
		struct myfs_dir_entry *de1 = (struct myfs_dir_entry *)((char *)de + name_len);
		de1->rec_len = cpu_to_le32(rec_len - name_len);
		de->rec_len = cpu_to_le32(name_len);
		de = de1;
	}
	de->name_len = namelen;
	memcpy(de->name, name, namelen);
	de->inode = cpu_to_le32(inode->i_ino);
	myfs_set_inode_type(de, inode);
	printk(KERN_ALERT "myfs: creating new entry (ino = %u) (name_len = %d) (rec_len = %d) of type %d.\n",de->inode, de->name_len, de->rec_len, de->file_type);
	err = myfs_commit_chunk(page, pos, rec_len);
	dir->i_mtime = dir->i_ctime = CURRENT_TIME_SEC;
	mark_inode_dirty(dir);
page_put:
	myfs_put_page(page);
out:
	return err;
page_unlock:
	unlock_page(page);
	goto page_put;
}

/*!
	функция чтения директории
*/
static int myfs_readdir(struct file *filep, void *dirent, filldir_t filldir)
{
	loff_t pos = filep->f_pos;
	struct inode *inode = filep->f_path.dentry->d_inode;
	unsigned int offset = pos & ~PAGE_CACHE_MASK;
	unsigned long pages = myfs_inode_pages(inode);
	unsigned n = pos >> PAGE_CACHE_SHIFT;
	if (pos > inode->i_size)
		return 0;
		
	printk(KERN_ALERT "myfs: readdir called at pos %llu and offset %u.\n", pos, offset);
	
	for(;n < pages; n++, offset = 0)
	{
		char *kaddr, *limit;
		struct myfs_dir_entry *de;
		struct page *page = myfs_get_page(inode, n);
		if (IS_ERR(page))
		{
			printk(KERN_ALERT "myfs: bad page (%u) found in inode %lu.\n", n, inode->i_ino);
			filep->f_pos += PAGE_CACHE_SIZE - offset;
			return PTR_ERR(page);
		}

		kaddr = page_address(page);
		de = (struct myfs_dir_entry *)(kaddr + offset);

		limit = kaddr + myfs_last_byte_for_page(inode, n) - calc_reclen_from_len(1);
		for (; (char *)de <= limit; de = (struct myfs_dir_entry *)((char *)de + de->rec_len))
		{
			if (!de->rec_len)
			{
				printk(KERN_ALERT "myfs: zero length rec_len for inode (%d) \"%s\".\n", de->inode, de->name);
				myfs_put_page(page);
				return -EIO;
			}
			if (de->inode)
			{
				int over;
				offset = (char *)de - kaddr;
				printk(KERN_ALERT "myfs: got entry \"%s\"\n",de->name);
				over = filldir(dirent, de->name, de->name_len, (n<<PAGE_CACHE_SHIFT)|offset,
						de->inode, de->file_type);
				if(over)
				{
					myfs_put_page(page);
					return 0;
				}
			}
			filep->f_pos += de->rec_len;
		}
		myfs_put_page(page);
	}
	return 0;
}

/*!
	функция поиска элемента в директории
*/
struct myfs_dir_entry *myfs_find_dentry(struct inode *dir, struct qstr *child, struct page **respage)
{
	struct myfs_dir_entry *de = NULL;
	int n, pages = myfs_inode_pages(dir);
	struct page *page;
	char *kaddr = NULL, *limit;

	printk(KERN_ALERT "myfs: trying to find \"%s\" in dir ino (%lu).\n", child->name, dir->i_ino);
	for (n = 0; n <= pages; n++)
	{
		page = myfs_get_page(dir, n);
		if (IS_ERR(page))
		{
			printk(KERN_ALERT "myfs: error reading page# (%d) of inode %lu.\n", n, dir->i_ino);
			goto out;
		}
		kaddr = page_address(page);
		limit = kaddr + myfs_last_byte_for_page(dir, n);
		de = (struct myfs_dir_entry *)kaddr;
		for (;(char *)de < limit ; de = (struct myfs_dir_entry *)((char *)de + de->rec_len))
		{
			if (!de->rec_len)
			{
				printk(KERN_ALERT "myfs: zero length rec_len for inode (%d) \"%s\".\n", de->inode, de->name);
				myfs_put_page(page);
				goto out;
			}
			if (myfs_match(child->len, child->name, de))
			{
				*respage = page;
				return de;
			}
		}
	}
out:
	return NULL;
}

/*!
	функция возвращает номер и-узла элемента директории по его имени
*/
unsigned int myfs_inode_by_name(struct inode *dir, struct qstr *child)
{
	unsigned ino = 0;
	struct myfs_dir_entry *dentry;
	struct page *page;
	dentry = myfs_find_dentry(dir, child, &page);
	if (dentry && !(IS_ERR(dentry)))
	{
		ino = le32_to_cpu(dentry->inode);
		myfs_put_page(page);
	}
	return ino;
}

/*!
	функция удаления элемента директории
*/
int myfs_delete_entry (struct myfs_dir_entry *dir, struct page *page)
{
	struct myfs_dir_entry *old, *cur;
	char *kaddr = page_address(page);
	int err = -ENOENT;
	struct address_space *mapping = page->mapping;
	struct inode *inode = mapping->host;
	int pos, from = (((char *)dir) - kaddr) & ~(inode->i_sb->s_blocksize - 1);
	int to = (((char *)dir) - kaddr) + calc_reclen_from_len(dir->name_len);
	old = cur= (struct myfs_dir_entry *)kaddr;

	for (;cur < dir;cur= (struct myfs_dir_entry *)((char *)cur+ cur->rec_len))
	{
		printk(KERN_ALERT "myfs: ino (%d - %s).\n",cur->inode, cur->name);
		old = cur;
	}
	if ((char *)cur!= kaddr)
	{
		old->rec_len += cur->rec_len;
		cur->inode = 0;
	}
	pos = page_offset(page) + from;
	lock_page(page);
	err = __myfs_write_begin(NULL, mapping, pos, to - from, 0, &page, NULL);

	err = myfs_commit_chunk(page, pos, to - from);
	mark_inode_dirty(inode);
	myfs_put_page(page);
	return err;
}

/*!
	в данной структуре хранятся ссылки на функции работы с директориями
*/
const struct file_operations myfs_dir_operations = {
	.llseek = generic_file_llseek,
	.read = generic_read_dir,
	.readdir = myfs_readdir,
};
